const React = require("react");

// 收集所有需要异步加载的组件
const ALL_INITIALIZERS = [];

// 预加载单个异步组件
function load(loader){
    let promise = loader();
    let state = {
        loading: true,
        loaded: null,
        error: null,
    }
    state.promise = promise.then(loaded => {
        state.loading = false;
        state.loaded = loaded;
        return loaded;
    }).catch(err => {
        state.loading = false;
        state.error = err;
        throw err;
    })
    return state;
}

function resolve(obj) {
    return obj && obj.__esModule ? obj.default : obj;
}
  
function render(loaded, props) {
    return React.createElement(resolve(loaded), props);
}

// 创建异步加载高阶组件
function createLoadableComponent(loadFn, options){
    if (!options.loading) {
        throw new Error("react-loadable requires a `loading` component");
    }
    let opts = Object.assign({
        loader: null,
        loading: null,
        delay: 200,
        timeout: null,
        render,
    }, options);

    let res = null;

    function init(){
        if(!res){
            res = loadFn(options.loader);
            return res.promise;
        }
    }

    ALL_INITIALIZERS.push(init);

    class LoadableComponent extends React.Component{
        constructor(props){
            super(props);
            init();
            this.state = {
                error: res.error,
                postDelay: false,
                timedOut: false,
                loading: res.loading,
                loaded: res.loaded
            }
            this._delay = null;
            this._timeout = null;
        }

        componentWillMount(){
            this._mounted = true;
            this._loadModule();
        }

        _loadModule(){
            if(!res.loading) return;
            if(typeof opts.delay === 'number'){
                if(opts.delay === 0){
                    this.setState({pastDelay: true});
                }else{
                    this._delay = setTimeout(()=>{
                        this.setState({pastDelay: true});
                    }, opts.delay)
                }
            }

            if(typeof opts.timeout === 'number'){
                this._timeout = setTimeout(()=>{
                    this.setState({timedOut: true});
                }, opts.timeout)
            }

            let update = () => {
                if(!this._mounted) return;
                this.setState({
                    error: res.error,
                    loaded: res.loaded,
                    loading: res.loading,
                });
            }

            res.promise.then(()=>{
                update()
            }).catch(err => {
                update()
            })
        }


        // 重新加载异步组件
        retry(){
            this.setState({
                error: null,
                timedOut: false,
                loading: false,
            });
            res = loadFn(opts.loader);
            this._loadModule();
        }

        static preload(){
            init()
        }


        componentWillUnmount(){
            this._mounted = false;
            clearTimeout(this._delay);
            clearTimeout(this._timeout);
        }

        render(){
            const {loading, error, pastDelay, timedOut, loaded} = this.state;
            if(loading || error){
                return React.createElement(opts.loading, {
                    isLoading: loading,
                    pastDelay: pastDelay,
                    timedOut: timedOut,
                    error: error,
                    retry: this.retry.bind(this),
                })
            }else if(loaded){
                return opts.render(loaded, this.props);
            }else{
                return null;
            }
        }

        
    }

    return LoadableComponent;
}

function Loadable(opts){
    return createLoadableComponent(load, opts);
}

Loadable.preloadAll = function(){
    let promises = [];
    while(ALL_INITIALIZERS.length){
        const init = ALL_INITIALIZERS.pop();
        promises.push(init())
    }
    return Promise.all(promises);
}

export default Loadable;